package com.personalkeeper;

import java.util.ArrayList;
import java.util.Date;

import com.personalkeeper.PersonalKeeper.Settings;
import com.personalkeeper.PersonalKeeper.Utils;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.util.Log;
import android.widget.RemoteViews;

/*
 * Service gets notification on location updates, if there is clients
 * bound to service, then it sends updated location to them.
 *
 * Service switches between 3 polling modes: ACTIVE, PASSIVE, NOT_POLLING.
 *
 * Default polling mode is PASSIVE, service switches to ACTIVE mode when
 * there is client bound to service, and switches back to PASSIVE when they
 * unbound.
 *
 * NOT_POLLING mode disables location update notification from GPS. It's not
 * used. Service use it only in 'onDestroy' method to disable notifications
 */
public class GPSPollingService extends Service implements LocationListener {

    // incoming msgs: argument passed through 'replyTo'
    public static final int MSG_REGISTER_CLIENT = 1;
    public static final int MSG_UNREGISTER_CLIENT = 2;

    // outgoing msgs: arguments = object
    public static final int MSG_SMS_RECEIVED = 3;
    public static final int MSG_LOCATION_CHANGED = 4;

    // outgoing msgs: arguments = arg1, arg2
    public static final int MSG_CONFIGURATION_CHANGED = 5;

    private static final int DEFAULT_PASSIVE_POLL_PERIOD = 60 * 1000;
    private static final int DEFAULT_ACTIVE_POLL_PERIOD = 5 * 1000;

    private enum PollingMode { PASSIVE, ACTIVE, NOT_POLLING }

    private class IncomingHandler extends Handler {

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case MSG_REGISTER_CLIENT:
                if (msg.replyTo != null) {
                    mClients.add(msg.replyTo);
                    log("client registred");
                } else
                    log("register client request denied because sender didn't set 'replyTo' field");
                break;

            case MSG_UNREGISTER_CLIENT:
                if (msg.replyTo != null) {
                    if (mClients.remove(msg.replyTo) == false)
                        log("can't remove messenger from list");
                    else
                        log("client unregistred");
                } else
                    log("unregister client request denied because sender didn't set 'replyTo' field");
                break;

            case MSG_CONFIGURATION_CHANGED:
                rereadConfiguration();
                break;

            default:
                log(String.format("Unknown message: %d", msg.what));
                super.handleMessage(msg);
            }
        }
    }

    private int mPassivePollPeriod = DEFAULT_PASSIVE_POLL_PERIOD;
    private int mActivePollPeriod = DEFAULT_ACTIVE_POLL_PERIOD;
    private PollingMode mPollingMode = PollingMode.NOT_POLLING;
    private LocationManager mLocationManager;
    private DatabaseWriter mWriter;
    private final Messenger mMessenger = new Messenger(new IncomingHandler());
    private final ArrayList<Messenger> mClients = new ArrayList<Messenger>();
    private SharedPreferences mPrefs;
    private boolean mIsTracking;
    private boolean mUnexpectedServiceShutdownDetected = false;
    private double mDistancePassed = 0.0;

    private static final String TAG = GPSPollingService.class.toString();
    private static void log(String msg) {
        Log.d(TAG, msg);
    }

    private void setPollingMode(PollingMode mode) {
        if (mPollingMode != mode) {
            //log(String.format("switch polling mode: %s", mode.toString()));

            if (mLocationManager == null)
                mLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);

            if (mode == PollingMode.ACTIVE)
                flushLocationBuffer();

            if (mode == PollingMode.NOT_POLLING)
                mLocationManager.removeUpdates(this);
            else {
                mLocationManager.requestLocationUpdates(
                    LocationManager.GPS_PROVIDER,
                    (mode == PollingMode.ACTIVE) ? mActivePollPeriod : mPassivePollPeriod,
                    0,
                    this);
            }
            mPollingMode = mode;
        }
    }

    private void setTracking(boolean onOff) {
        if (mIsTracking == true && onOff == false) {
            // turn off
            flushLocationBuffer();
            mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
            long trackingStartTime = mPrefs.getLong(Settings.TRACKING_START_TIME, 0);
            long trackingEndTime = System.currentTimeMillis();

            if (trackingStartTime < trackingEndTime && trackingStartTime != 0) {
                double distance = 0.0;

                if (mUnexpectedServiceShutdownDetected) {
                    //log("calculating real distance");
                    distance = StatisticsCalculator.calculateStatistics(
                                   this,
                                   trackingStartTime,
                                   trackingEndTime).getDistance();
                    mUnexpectedServiceShutdownDetected = false;
                } else {
                    //log("using precalculated distance");
                    distance = mDistancePassed;
                }

                if (distance > 0) {
                    mWriter.insert(new DatabaseWriter.Session(
                                       new Date(trackingStartTime),
                                       new Date(trackingEndTime),
                                       distance));
                }
            } else {
                // default value has been returned - WTF???
                // if not default that's still WTF???
                Log.wtf(TAG, "tracking length 0 msec or no tracking_start_time value");
            }
        } else if (mIsTracking == false && onOff == true) {
            // turn on
            mDistancePassed = 0.0;
        }

        mIsTracking = onOff;
    }

    private void rereadConfiguration() {
        mPrefs = PreferenceManager.getDefaultSharedPreferences(this);

        mActivePollPeriod = Settings.loadActivePollPeriod(mPrefs);
        mPassivePollPeriod = Settings.loadPassivePollPeriod(mPrefs);
        setTracking(mPrefs.getBoolean(Settings.IS_TRACKING, false));
        setPollingMode(mPollingMode);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        mWriter = new DatabaseWriter(this);
        PreferenceManager.setDefaultValues(this, R.xml.preference_screen, false);
        mPrefs = PreferenceManager.getDefaultSharedPreferences(this);

        // get 'is_tracking' value on creation, so we could say if service got crashed unexpectedly
        mUnexpectedServiceShutdownDetected = mPrefs.getBoolean(Settings.IS_TRACKING, false);
        if (mUnexpectedServiceShutdownDetected)
            Log.w(TAG, "Service crash detected");

        mIsTracking = false;
        mPollingMode = PollingMode.NOT_POLLING;

        try {
            rereadConfiguration();
        } catch (ClassCastException e) {
            log("failed to read configuration, setting all preferences to default values");
            PreferenceManager.setDefaultValues(this, R.xml.preference_screen, true);
            rereadConfiguration();
        } catch (NumberFormatException e) {
            log("failed to read configuration, setting all preferences to default values");
            PreferenceManager.setDefaultValues(this, R.xml.preference_screen, true);
            rereadConfiguration();
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (intent != null && intent.getAction() != null) {
            if (intent.getAction().equals(PersonalKeeper.ACTION_SMS_RECEIVED)) {
                if (mClients.size() == 0) {
                    DatabaseWriter.Marker marker = DatabaseWriter.Marker.fromBundle(intent.getExtras());
                    if (marker != null)
                        mWriter.insert(marker);
                }
                if (intent.getBooleanExtra(PersonalKeeper.REQUEST, false))
                    showNotification(intent.getExtras());
                else
                    sendToClients(Message.obtain(null, MSG_SMS_RECEIVED, intent.getExtras()));

                // if service was created to handle SMS message and it wasn't working at the time
                // stop it
                if (mPollingMode == PollingMode.NOT_POLLING)
                    stopSelf();
            }
        }

        if (mIsTracking && mPollingMode == PollingMode.NOT_POLLING)
            setPollingMode(PollingMode.PASSIVE);

        return START_STICKY;
    }

    @Override
    public IBinder onBind(Intent intent) {
        setPollingMode(PollingMode.ACTIVE);
        return mMessenger.getBinder();
    }

    @Override
    public void onRebind(Intent intent) {
        setPollingMode(PollingMode.ACTIVE);
    }

    @Override
    public boolean onUnbind(Intent intent) {
        //log("all clients unbound");
        setPollingMode(PollingMode.PASSIVE);
        return true;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        log("onDestroy()");
        setTracking(false);
        //mPrefs.edit().putBoolean(Settings.IS_TRACKING, false).commit();
        // switch to NOT_POLLING mode to disable notifications from GPS
        // Note: without this LocationListener keeps receiving updates
        // even if both service and activity are dead if process still alive
        setPollingMode(PollingMode.NOT_POLLING);
        flushLocationBuffer();
    }

    private void sendToClients(Message msg) {
        // iterate from the end to begin, because elements can be removed
        for (int i = mClients.size() - 1; i >= 0; i--) {
            try {
                mClients.get(i).send(msg);
            } catch (RemoteException e) {
                mClients.remove(i);
            }
        }
    }

    // location listener methods
    @Override
    public void onStatusChanged(String provider, int status, Bundle extras) {
        //log("onStatusChanged(): " + provider);
    }

    @Override
    public void onProviderEnabled(String provider) {
       //log("onProviderEnabled(): " + provider);
    }

    @Override
    public void onProviderDisabled(String provider) {
        //log("onProviderDisabled(): " + provider);
    }

    private static final int BULK_INSERT_SIZE = 20;
    private ArrayList<Location> mLocationBuffer = new ArrayList<Location>(BULK_INSERT_SIZE);
    private Location mLastLocationUpdate;

    @Override
    public void onLocationChanged(Location location) {
        // locations have strange timestamp on emulator
        location.setTime(System.currentTimeMillis());

        if (mIsTracking)
        {
            double distanceBetween = 0;
            if (mLastLocationUpdate != null) {
                distanceBetween = Utils.distanceBetween(mLastLocationUpdate, location);
                mDistancePassed += distanceBetween;
            }

            mLocationBuffer.add(location);
            if (mLocationBuffer.size() == BULK_INSERT_SIZE)
                flushLocationBuffer();
            mLastLocationUpdate = location;
        }
        sendToClients(Message.obtain(null, MSG_LOCATION_CHANGED, location));
    }

    private void flushLocationBuffer() {
        if (mLocationBuffer.size() > 0) {
            mWriter.insert(mLocationBuffer);
            mLocationBuffer.clear();
        }
    }

    private void showNotification(Bundle extras) {
        if (extras != null) {
            Notification notification = new Notification(
                R.drawable.icon,
                getString(R.string.app_name),
                System.currentTimeMillis());
            RemoteViews contentView = new RemoteViews(getPackageName(), R.layout.custom_layout);

            contentView.setImageViewResource(R.id.image, R.drawable.icon);
            contentView.setTextViewText(R.id.title, getString(R.string.app_name));
            contentView.setTextViewText(R.id.text, getString(R.string.request_notification_text));
            notification.contentView = contentView;

            Intent notificationIntent = new Intent(this, MapDisplay.class);
            notificationIntent.putExtras(extras);
            notificationIntent.setAction(PersonalKeeper.ACTION_REQUEST_FOR_LOCATION);
            PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
            notification.contentIntent = contentIntent;
            notification.flags |= Notification.FLAG_AUTO_CANCEL;
            NotificationManager mManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
            //TODO: create constant for notification id
            mManager.notify(1010, notification);
        }
    }
}
